﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DeepZoomPivotConstructor.VisualLib;
using DeepZoomPivotConstructor.VisualCacheInterfaces;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Reflection;

namespace DeepZoomPivotConstructor.VisualCacheXMLDirectory
{
    /// <summary>
    /// Implements a visual cache using a binary file as the database.
    /// </summary>
    public class VisualCacheDirectoryDB : IVisualCacheDB
    {
        private DirectoryInfo _dbLocation = null;

        /// <summary>
        /// Get/Set the new database location.
        /// </summary>
        public DirectoryInfo DBLocation
        {
            get { return _dbLocation; }
            set
            {
                MoveDB(_dbLocation, value);
            }
        }

        public VisualCacheDirectoryDB (DirectoryInfo dbLoc)
        {
            DBLocation = dbLoc;

            ///
            /// Tune up assembly resolution. BinaryFormatter doesn't know
            /// to look at loaded assemblies to see if the object it is attempting
            /// to load is there. By virtue of where we are in the call graph, when
            /// we are deserializing the metadata and the render object we will
            /// have already loaded the IRenderItem. That assembly shoudl contain the
            /// the object definition. Sadly, BinaryFormatter doesn't know how to
            /// get this on its own. So, we help things out...
            /// 
            /// See: http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/e5f0c371-b900-41d8-9a5b-1052739f2521/
            /// 

            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

        }

        /// <summary>
        /// Move the location of the database from one place to the other.
        /// </summary>
        /// <param name="_dbLocation"></param>
        /// <param name="value"></param>
        private void MoveDB(DirectoryInfo oldLoc, DirectoryInfo newLoc)
        {
            ///
            /// No one else had better be trying to mess with this!
            /// 
            lock (this)
            {
                if (oldLoc != null && oldLoc.Exists)
                {
                    if (!newLoc.Exists)
                    {
                        newLoc.Create();
                        newLoc.Refresh();
                    }
                    foreach (var f in oldLoc.GetFiles())
                    {
                        f.MoveTo(newLoc.FullName + "\\" + f.Name);
                    }
                    oldLoc.Delete();
                }

                ///
                /// Now, set to the new location so we can find it when we get the next request
                /// We have to do this while still under the lock!
                /// 

                _dbLocation = newLoc;
            }
        }

        /// <summary>
        /// Assembly resolution has failed. But that may be because we've not checked
        /// all of the currently loaded assemblies. We scan through the list and look
        /// for a type that might do us ok. We drop version information (bad us).
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            string sShortAssemblyName = args.Name.Split(',')[0];

            var matchingAss = from a in AppDomain.CurrentDomain.GetAssemblies()
                              where sShortAssemblyName == a.FullName.Split(',')[0]
                              select a;

            return matchingAss.FirstOrDefault();
        }

        /// <summary>
        /// Returns a hit for the cache value.
        /// </summary>
        /// <param name="hash"></param>
        /// <returns></returns>
        public CacheDBInfo FindHit(long hash)
        {
            lock (this)
            {
                return LoadCacheForHash(hash);
            }
        }

        /// <summary>
        /// Update/Add a new item to the hash. We expect no hash collisions, I'm afraid.
        /// So lets hope none happen! :-)
        /// </summary>
        /// <param name="info"></param>
        public void Save(CacheDBInfo info)
        {
            lock (this)
            {
                SaveCacheForHash(info, info.ComposeInfo.Hash);
            }
        }

        /// <summary>
        /// Given a list of objects, write them out. We expect the object to be locked
        /// already, so nothing funky happens here with locks
        /// </summary>
        /// <param name="infosForHash"></param>
        /// <param name="p"></param>
        private void SaveCacheForHash(CacheDBInfo infosForHash, long hash)
        {
            FileInfo dbFile = DBFileInfoFromHash(hash);
            if (!dbFile.Directory.Exists)
            {
                dbFile.Directory.Create();
            }
            using (Stream wtr = dbFile.Create())
            {
                BinaryFormatter fmt = new BinaryFormatter();
                fmt.Serialize(wtr, infosForHash);
                wtr.Close();
            }
        }

        /// <summary>
        /// Load a cache back from a file. null if the files dosen't exist!
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private CacheDBInfo LoadCacheForHash(long hash)
        {
            FileInfo dbFile = DBFileInfoFromHash(hash);
            if (!dbFile.Exists)
            {
                return null;
            }

            try
            {
                using (Stream rdr = dbFile.OpenRead())
                {
                    BinaryFormatter fmt = new BinaryFormatter();
                    CacheDBInfo dbinfo = fmt.Deserialize(rdr) as CacheDBInfo;
                    rdr.Close();
                    return dbinfo;
                }
            } catch
            {
                // If we can't read the cache file, then we can't recover the data. So this is a miss.
                return null;
            }
        }

        /// <summary>
        /// Given a hash number turn it into a db file.
        /// </summary>
        /// <param name="hash"></param>
        /// <returns></returns>
        private FileInfo DBFileInfoFromHash(long hash)
        {
            return new FileInfo(DBLocation.FullName + "\\" + hash.ToString());
        }
    }
}
